feat(agentex): add register-build endpoint and BUILD_ONLY agent status#256
feat(agentex): add register-build endpoint and BUILD_ONLY agent status#256rpatel-scale wants to merge 3 commits into
Conversation
Create the agent registry row at build time instead of only at deploy
time, so an agent resource (and id) exists before deployment and can be
permissioned/shared up front.
- Add `BUILD_ONLY` ("BuildOnly") value to AgentStatus (entity + schema)
and an alembic migration adding it to the Postgres `agentstatus` enum.
- Add `POST /agents/register-build`: creates the agent row without an
acp_url, in BUILD_ONLY status, and grants the caller access. Unlike
/register it does not mint an API key. Idempotent by name so a rebuild
never clobbers a live deployment.
- Deploy-time /register continues to flip the agent to READY and set the
acp_url, unchanged.
- Regenerate openapi.yaml for the new endpoint.
Part of the agent-creation/authz work tracked in the Agentex agent
creation Authz doc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
✱ Stainless preview buildsThis PR will update the openapi python typescript Edit this comment to update them. They will appear in their respective SDK's changelogs. ✅ agentex-sdk-openapi studio · code · diff
✅ agentex-sdk-typescript studio · code · diff
✅ agentex-sdk-python studio · code · diff
This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push. |
| await authorization_service.grant( | ||
| AgentexResource.agent(agent_entity.id), | ||
| principal_context=request.principal_context, | ||
| ) | ||
| return Agent.model_validate(agent_entity) |
There was a problem hiding this comment.
Grant issued unconditionally on idempotent return
authorization_service.grant is called regardless of whether the use case created a new agent or returned an existing one. In the idempotent path (register_build found an existing agent by name), the current caller is granted access to an agent they did not create — without any modification to that agent. A principal with wildcard create permission can claim a grant on any existing agent simply by knowing its name and calling this endpoint. The /register endpoint has a comparable pattern but requires a live acp_url, giving stronger proof of ownership; register-build has no such constraint, lowering the bar considerably.
Prompt To Fix With AI
This is a comment left during a code review.
Path: agentex/src/api/routes/agents.py
Line: 256-260
Comment:
**Grant issued unconditionally on idempotent return**
`authorization_service.grant` is called regardless of whether the use case created a new agent or returned an existing one. In the idempotent path (`register_build` found an existing agent by name), the current caller is granted access to an agent they did not create — without any modification to that agent. A principal with wildcard `create` permission can claim a grant on any existing agent simply by knowing its name and calling this endpoint. The `/register` endpoint has a comparable pattern but requires a live `acp_url`, giving stronger proof of ownership; `register-build` has no such constraint, lowering the bar considerably.
How can I resolve this? If you propose a fix, please make it concise.In the deployment-scoped registration flow (deployment_id present), /register only updates the deployment record and deliberately leaves the agent row untouched, deferring acp_url/status changes to promotion. But promote() only set the agent's acp_url and production_deployment_id, never its status, so a build-only agent created via /register-build would keep its BUILD_ONLY status forever even after going live. Flip a BUILD_ONLY agent to READY (with status_reason/registered_at) when its deployment is promoted, mirroring the legacy /register status flip. Other statuses (e.g. UNHEALTHY) are left untouched. Add an integration test covering the full build -> deployment-scoped register -> promote path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop internal storage details (agent row, acp_url, BUILD_ONLY status) and the API-key aside from the public OpenAPI description; describe the behavior from the consumer's perspective. Regenerate openapi.yaml. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Linear: AGX1-308
Context: https://app.notion.com/p/Agentex-agent-creation-with-Authz-36f904d6e6cb80bc9262ddb0207a42e8
What
Adds the first building block for creating an agent's registry row at build time instead of only at deploy time, so an agent resource (and
id) exists before deployment and can be permissioned/shared up front.BUILD_ONLYagent status ("BuildOnly"), added toAgentStatusin both the domain entity and the API schema, plus an Alembic migration adding the value to the Postgresagentstatusenum (ALTER TYPE ... ADD VALUE IF NOT EXISTS, mirroringadd_unhealthy_status).POST /agents/register-build— creates the agent row without anacp_url(no running pod yet), inBUILD_ONLYstatus, and grants the caller access. Unlike/register, it does not mint an API key. Idempotent by name, so re-building an existing agent never clobbers a live deployment's status/acp_url./registeris unchanged — registering with the agent'sagent_idstill flips it toREADYand sets theacp_url.openapi.yaml.Why
Today the agent registry row is only created at deploy/register time. Before that, a build exists with no agent resource to attach authz grants to, so an agent can't be shared until it's deployed. Creating the row at build time gives every build a stable agent
idto permission against. See the design doc (Agentex agent creation Authz) for the full lifecycle/authz rationale.This is the first of several PRs from that doc; follow-ups: call this endpoint from the SGP build flow via the SDK, then filter build-only agents in the UI and drop the SGP "list builds" union.
Lifecycle
Tests
New integration tests in
tests/integration/api/agents/test_agents_api.py:register-buildcreates aBuildOnlyagent with noacp_urland no API key, retrievable like any agent.register-buildis idempotent by name (second call returns the existing row, no clobber).BUILD_ONLYagent is promoted toReadyby a subsequent/registerwith itsagent_id.Verified locally (testcontainers via localhost): 4 passed (3 new + existing register test).
ruff,ruff-format, and the migration-safety linter all pass.🤖 Generated with Claude Code
Greptile Summary
This PR introduces the first step of the build-time agent lifecycle: a new
POST /agents/register-buildendpoint that creates an agent row inBUILD_ONLYstatus (noacp_url, no API key) so the agent resource can be permissioned before any pod is deployed. It also adds the necessaryBUILD_ONLYdomain enum, Postgres migration, and logic inDeploymentRepository.promote()to atomically flip a build-only agent toREADYwhen its first deployment is promoted.BUILD_ONLYstatus added toAgentStatusin both the domain entity and API schema, along with an AlembicALTER TYPE … ADD VALUE IF NOT EXISTSmigration.POST /agents/register-buildis idempotent by name and mirrors the auth pattern of/register(check + grant), but omits API-key creation andacp_urlpopulation.DeploymentRepository.promote()extended to promote a BUILD_ONLY agent to READY on its first deployment promotion; integration tests cover both the legacy-register and deployment-scoped promotion flows.Confidence Score: 4/5
Safe to merge after addressing the soft-deleted agent case in register_build.
The idempotency guard in register_build calls self.agent_repo.get(name=name) using the base repository method, which does not filter out soft-deleted rows. A previously deleted agent with the same name would be returned as-is, giving the caller a 200 with status Deleted instead of BuildOnly. All other changes — the migration, the schema enum, the promotion logic in deployment_repository, and the integration tests — look correct.
agentex/src/domain/use_cases/agents_use_case.py — the register_build idempotency guard needs to handle the DELETED status before returning the existing row.
Important Files Changed
Sequence Diagram
sequenceDiagram participant Client participant Route as /agents/register-build participant UseCase as AgentsUseCase participant Repo as AgentRepository participant Auth as AuthorizationService Client->>Route: POST /agents/register-build Route->>Auth: "check(agent(*), create)" Auth-->>Route: ok Route->>UseCase: register_build(name, ...) UseCase->>Repo: "get(name=name)" alt Agent exists Repo-->>UseCase: existing AgentEntity UseCase-->>Route: existing agent else ItemDoesNotExist UseCase->>Repo: create(BUILD_ONLY agent) Repo-->>UseCase: new BUILD_ONLY AgentEntity UseCase-->>Route: new agent end Route->>Auth: grant(agent(id), principal) Route-->>Client: "200 Agent {status: BuildOnly}"Prompt To Fix All With AI
Reviews (3): Last reviewed commit: "docs(agentex): simplify register-build e..." | Re-trigger Greptile